using System;
using System.Collections.Generic;
using System.Text;
using CookComputing.XmlRpc;
using System.IO;
using System.Reflection;
using System.Configuration;
using System.Security.Policy;

namespace EnsoNetServer
{
    /// <summary>
    /// Dispatches command requests.
    /// </summary>
    public class CommandServer : MarshalByRefObject
    {
        #region Private Members

        /// <summary>
        /// Logger.
        /// </summary>
        private static readonly log4net.ILog _logger = log4net.LogManager.GetLogger("server");
        /// <summary>
        /// Enso end point.
        /// </summary>
        private static IEnso _enso;
        /// <summary>
        /// Commandlets domain.
        /// </summary>
        private static AppDomain _commandDomain;
        /// <summary>
        /// Command handlers mapping.
        /// </summary>
        private static Dictionary<Command, ICommandlet> _commandHandlers = new Dictionary<Command, ICommandlet>();
        /// <summary>
        /// Notifies about changes in the file system.
        /// </summary>
        private static FileSystemWatcher _watcher;
        /// <summary>
        /// Indicates whether it is the first loading of commands.
        /// </summary>
        private static bool _first = true;

        #endregion

        #region RPC Methods

        /// <summary>
        /// Dispatches command to a specific commandlet.
        /// </summary>
        /// <param name="command">Command name.</param>
        /// <param name="postfix">Postfix.</param>
        [XmlRpcMethod("callCommand")]
        public void CallCommand(string command, string postfix)
        {
            lock (_commandHandlers)
            {
                foreach (KeyValuePair<Command, ICommandlet> handler in _commandHandlers)
                {
                    if (handler.Key.Name == command && handler.Key.Handler != null)
                    {
                        try
                        {
                            handler.Key.Handler(_enso, command, postfix);
                        }
                        catch (Exception e)
                        {
                            _logger.Error("Call command", e);
                        }
                    }
                }
            }
        }

        #endregion

        #region Utility Methdods

        /// <summary>
        /// Connects to Enso endpoint.
        /// </summary>
        public static IEnso ConnectEnso()
        {
            _enso = XmlRpcProxyGen.Create<IEnso>();
            ((IXmlRpcProxy)_enso).Url = "http://127.0.0.1:11374";

            return _enso;
        }

        /// <summary>
        /// Enumerates supported commands.
        /// </summary>
        /// <returns>Enumerator.</returns>
        public static IEnumerable<string> EnumerateCommands()
        {
            foreach (KeyValuePair<Command, ICommandlet> handler in _commandHandlers)
            {
                yield return handler.Key.Name + " - " + Path.GetFileName(handler.Value.GetType().Assembly.CodeBase);
            }
        }

        /// <summary>
        /// Initializes track of file system changes.
        /// </summary>
        public static void SetupFileSystemWatcher()
        {
            _watcher = new FileSystemWatcher(GetCommandletsDirectory(), "*.dll");
            _watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime;
            _watcher.Changed += new FileSystemEventHandler(Watcher_Changed);
            _watcher.Created += new FileSystemEventHandler(Watcher_Changed);

            _watcher.EnableRaisingEvents = true;
        }

        /// <summary>
        /// Unloads everything.
        /// </summary>
        public static void UnloadAll()
        {
            if (_commandDomain != null)
            {
                foreach (KeyValuePair<Command, ICommandlet> handler in _commandHandlers)
                {
                    try
                    {
                        _enso.UnregisterCommand(GetUrl(), handler.Key.Name);
                    }
                    catch (Exception e)
                    {
                        _logger.Error("Unregister command", e);
                    }
                }

                AppDomain.Unload(_commandDomain);
            }
        }

        /// <summary>
        /// Loads a new list of commandlets.
        /// </summary>
        public static void LoadCommandlets()
        {
            lock (_commandHandlers)
            {
                if (!_first)
                {
                    _enso.DisplayMessage("<p>.NET commands server is reloading</p>");
                }
                _first = false;

                UnloadAll();

                AppDomainSetup setup = new AppDomainSetup();

                setup.ShadowCopyFiles = "true";

                _commandDomain = AppDomain.CreateDomain("Commandlets", new Evidence(AppDomain.CurrentDomain.Evidence), setup);
                _commandHandlers.Clear();

                try
                {
                    ProxyLoader loader = _commandDomain.CreateInstanceAndUnwrap("EnsoNetServer", "EnsoNetServer.ProxyLoader") as ProxyLoader;

                    foreach (string file in Directory.GetFiles(GetCommandletsDirectory()))
                    {
                        try
                        {
                            foreach (ICommandlet commandletHandler in loader.CreateCommandlets(Path.GetFileNameWithoutExtension(file)))
                            {
                                Command[] commands = commandletHandler.GetCommands();
                                if (commands == null)
                                    continue;

                                foreach (Command command in commands)
                                {
                                    _commandHandlers[command] = commandletHandler;

                                    _enso.RegisterCommand(GetUrl(),
                                        command.Name, command.Description, command.Description, command.DecodePostfixType());
                                    
                                    if (command.PostfixType == PostfixType.Bounded)
                                        _enso.SetCommandValidPostfixes(GetUrl(), command.Name, command.Postfixes);
                                }
                            }
                        }
                        catch (Exception e)
                        {
                            _logger.Error("Load commandlets from " + file, e);
                        }
                    }
                }
                catch (Exception e)
                {
                    _logger.Error("Loading commandlets", e);
                }
            }
        }       

        #endregion

        #region Private Methods

        /// <summary>
        /// Gets local remoting end point url.
        /// </summary>
        /// <returns>Service Url.</returns>
        private static string GetUrl()
        {
            return ConfigurationManager.AppSettings["LocalEndpoint"];
        }

        private static string GetCommandletsDirectory()
        {
            return Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), ConfigurationManager.AppSettings["CommandletsDirectory"]);
        }

        private static void Watcher_Changed(object sender, FileSystemEventArgs e)
        {
            LoadCommandlets();
        }        

        #endregion
    }

    /// <summary>
    /// Helper class to load assembly into app domain without returning a reference to it.
    /// </summary>
    public class ProxyLoader : MarshalByRefObject
    {
        /// <summary>
        /// Logger.
        /// </summary>
        private static readonly log4net.ILog _logger = log4net.LogManager.GetLogger("server");

        public List<ICommandlet> CreateCommandlets(string assemblyName)
        {
            List<ICommandlet> commandlets = new List<ICommandlet>();
            Assembly assembly = Assembly.Load(assemblyName);
            foreach (Type type in assembly.GetExportedTypes())
            {
                if (typeof(ICommandlet).IsAssignableFrom(type) && typeof(MarshalByRefObject).IsAssignableFrom(type))
                {
                    ICommandlet commandlet = null;
                    try
                    {
                        commandlet = assembly.CreateInstance(type.FullName) as ICommandlet;                        
                    }
                    catch (Exception e)
                    {
                        _logger.Error("Could not load commandlet", e);
                    }

                    if (commandlet != null)
                        commandlets.Add(commandlet);
                }
            }

            return commandlets;
        }
    }
}
